﻿namespace Jsl.FxCop
{
    using System;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// Finds method calls that are more efficient if you use <see cref="char"/> instead of <see cref="string"/>.
    /// </summary>
    public class UseCharInsteadOfString : BaseIntrospectionRule
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="UseCharInsteadOfString"/> class.
        /// </summary>
        public UseCharInsteadOfString()
            : base(typeof(UseCharInsteadOfString).Name, "Jsl.FxCop.Rules", typeof(UseCharInsteadOfString).Assembly)
        {
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Checks the specified member.
        /// </summary>
        /// <param name="member">The member.</param>
        /// <returns>The problems found if any.</returns>
        public override ProblemCollection Check(Member member)
        {
            this.Visit(member);

            return this.Problems;
        }

        /// <summary>
        /// Visits the method call.
        /// </summary>
        /// <param name="call">The call being visited.</param>
        public override void VisitMethodCall(MethodCall call)
        {
            if (call == null)
            {
                throw new ArgumentNullException("call");
            }

            Method calledMethod = call.GetMethod();
            switch (calledMethod.DeclaringType.FullName)
            {
                case WellKnownTypeNames.BinaryWriter:
                case WellKnownTypeNames.StreamWriter:
                case WellKnownTypeNames.StringWriter:
                    this.ProcessWrite(call, calledMethod);
                    break;

                case WellKnownTypeNames.Console:
                case WellKnownTypeNames.TextWriter:
                case "System.CodeDom.Compiler.IndentedTextWriter":
                    this.ProcessWrite(call, calledMethod);
                    this.ProcessWriteLine(call, calledMethod);
                    break;

                case WellKnownTypeNames.String:
                    this.ProcessStringType(call, calledMethod);
                    break;

                case WellKnownTypeNames.StringBuilder:
                    this.ProcessStringBuilderType(call, calledMethod);
                    break;
            }

            base.VisitMethodCall(call);
        }
        #endregion Public Methods

        #region Private Methods
        private static bool CanFirstParameterOfStringIndexOfBeChar(Method calledMethod)
        {
            // We only care about IndexOf(string, ...).
            if (calledMethod.Parameters[0].Type.FullName != WellKnownTypeNames.String)
            {
                return false;
            }

            // The second parameter, if any, must be an int.
            if ((calledMethod.Parameters.Count > 1)
                && (calledMethod.Parameters[1].Type.FullName != WellKnownTypeNames.Int32))
            {
                return false;
            }

            // The third parameter, if any, must be an int.
            if ((calledMethod.Parameters.Count > 2)
                && (calledMethod.Parameters[2].Type.FullName != WellKnownTypeNames.Int32))
            {
                return false;
            }

            // The four parameter method doesn't support char.
            return calledMethod.Parameters.Count <= 3;
        }

        private static bool CanParametersOfStringReplaceByChar(Method calledMethod)
        {
            // It's possible to use char if both parameters are strings.
            return (calledMethod.Parameters.Count == 2)
                && (calledMethod.Parameters[0].Type.FullName == WellKnownTypeNames.String)
                && (calledMethod.Parameters[1].Type.FullName == WellKnownTypeNames.String);
        }

        private static bool IsRepeatedCharacterString(string literal)
        {
            var firstCharacter = literal[0];
            foreach (var character in literal)
            {
                if (firstCharacter != character)
                {
                    return false;
                }
            }

            return true;
        }

        private void ProcessStringIndexOf(MethodCall call, Method calledMethod)
        {
            if (CanFirstParameterOfStringIndexOfBeChar(calledMethod))
            {
                this.CheckForSingleReplacement(call, 0);
            }
        }

        private void ProcessStringReplace(MethodCall call, Method calledMethod)
        {
            if (CanParametersOfStringReplaceByChar(calledMethod))
            {
                string literal1;
                if (call.Operands[0].TryGetLiteral(out literal1)
                    && (literal1 != null)
                    && (literal1.Length == 1))
                {
                    string literal2;
                    if (call.Operands[1].TryGetLiteral(out literal2)
                        && (literal2 != null)
                        && (literal2.Length == 1))
                    {
                        this.Problems.Add(
                            new Problem(
                                this.GetNamedResolution("MultipleReplacement", literal1, literal2),
                                call));
                    }
                }
            }
        }

        private void ProcessStringBuilderReplace(MethodCall call, Method calledMethod)
        {
            if ((calledMethod.Parameters[0].Type.FullName == WellKnownTypeNames.String)
                && (calledMethod.Parameters[1].Type.FullName == WellKnownTypeNames.String))
            {
                string literal1;
                if (call.Operands[0].TryGetLiteral(out literal1)
                    && (literal1 != null)
                    && (literal1.Length == 1))
                {
                    string literal2;
                    if (call.Operands[1].TryGetLiteral(out literal2)
                        && (literal2 != null)
                        && (literal2.Length == 1))
                    {
                        this.Problems.Add(
                            new Problem(
                                this.GetNamedResolution("MultipleReplacement", literal1, literal2),
                                call));
                    }
                }
            }
        }

        private void ProcessStringBuilderType(MethodCall call, Method calledMethod)
        {
            switch (calledMethod.Name.Name)
            {
                case "Append":
                    this.ProcessStringBuilderAppend(call, calledMethod);
                    break;

                case "Insert":
                    this.ProcessStringBuilderInsert(call, calledMethod);
                    break;

                case "Replace":
                    this.ProcessStringBuilderReplace(call, calledMethod);
                    break;
            }
        }

        private void ProcessStringBuilderAppend(MethodCall call, Method calledMethod)
        {
            // We only care about the string argument.
            if ((calledMethod.Parameters.Count != 1)
                || (calledMethod.Parameters[0].Type.FullName != WellKnownTypeNames.String))
            {
                return;
            }

            string literal;
            if (call.Operands[0].TryGetLiteral(out literal)
                && (literal != null))
            {
                if (literal.Length == 1)
                {
                    this.Problems.Add(
                        new Problem(
                            this.GetNamedResolution("SingleReplacement", literal),
                            call));
                }
                else if (IsRepeatedCharacterString(literal))
                {
                    this.Problems.Add(
                        new Problem(
                            this.GetNamedResolution(
                                "StringBuilderAppendRepeatedCharacters",
                                literal,
                                literal[0],
                                literal.Length),
                            call));
                }
            }
        }

        private void ProcessStringBuilderInsert(MethodCall call, Method calledMethod)
        {
            // We only care about Inset(#, "x").
            if ((calledMethod.Parameters.Count != 2)
                || (calledMethod.Parameters[0].Type.FullName != WellKnownTypeNames.Int32)
                || (calledMethod.Parameters[1].Type.FullName != WellKnownTypeNames.String))
            {
                return;
            }

            this.CheckForSingleReplacement(call, 1);
        }

        private void ProcessStringType(MethodCall call, Method calledMethod)
        {
            switch (calledMethod.Name.Name)
            {
                case "IndexOf":
                case "LastIndexOf":
                    this.ProcessStringIndexOf(call, calledMethod);
                    break;

                case "Replace":
                    this.ProcessStringReplace(call, calledMethod);
                    break;
            }
        }

        private void CheckForSingleReplacement(MethodCall call, int operandIndex)
        {
            string literal;
            if (call.Operands[operandIndex].TryGetLiteral(out literal)
                && (literal != null)
                && (literal.Length == 1))
            {
                this.Problems.Add(
                    new Problem(
                        this.GetNamedResolution("SingleReplacement", literal),
                        call));
            }
        }

        private void ProcessWrite(MethodCall call, Method calledMethod)
        {
            if ((calledMethod.Parameters.Count == 1)
                && (calledMethod.Parameters[0].Type.FullName == WellKnownTypeNames.String)
                && (calledMethod.Name.Name == "Write"))
            {
                this.CheckForSingleReplacement(call, 0);
            }
        }

        private void ProcessWriteLine(MethodCall call, Method calledMethod)
        {
            if ((calledMethod.Parameters.Count == 1)
                && (calledMethod.Parameters[0].Type.FullName == WellKnownTypeNames.String)
                && (calledMethod.Name.Name == "WriteLine"))
            {
                this.CheckForSingleReplacement(call, 0);
            }
        }
        #endregion Private Methods
    }
}